/**
 * The contents of this file are subject to the license and copyright detailed in the LICENSE and NOTICE files at the root of the source tree and available online at
 * http://www.askosi.org/license.html
 */
package be.destin.util;

import java.io.File;
import java.io.UnsupportedEncodingException;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.Locale;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang3.StringEscapeUtils;

import be.destin.beans.Reflector;

/**
 * Functions for different small treatments (mainly starting from a String)
 * 
 * @author Christophe Dupriez
 *
 */
public class Util {

	/**
	 * Converts a String to boolean: "true" = true, "false" = false.
	 * 
	 * @param aString	string to be encoded in bool
	 * @param defBool	default value if string is null, empty or invalid
	 * @return	the boolean in the string
	 */
	public static boolean string2bool(String aString, boolean defBool) {
		if (aString == null)
			return defBool;
		if (aString.length() == 0)
			return defBool;
		try {
			return Boolean.valueOf(aString).booleanValue();
		} catch (NumberFormatException e) {
			return defBool;
		}
	}

	/**
	 * Converts a String to int.
	 * 
	 * @param aString	string to be parsed in 
	 * @param defInt	default value if string is null, empty or invalid
	 * @return	int number represented by the input string
	 */
	public static int string2int(String aString, int defInt) {
		if (aString == null)
			return defInt;
		if (aString.length() == 0)
			return defInt;
		try {
			return Integer.valueOf(aString).intValue();
		} catch (NumberFormatException e) {
			return defInt;
		}
	}

	/*
	 * Escape XML entities AND remove invalid or supplementary Unicode characters
	 * The set of characters from U+0000 to U+FFFF is sometimes referred to as the Basic Multilingual Plane (BMP). Characters whose code points are greater than U+FFFF are called supplementary characters. The Java platform uses the UTF-16 representation in char arrays and in the String and StringBuffer classes. In this representation, supplementary characters are represented as a pair of char values, the first from the high-surrogates range, (\uD800-\uDBFF), the second from the low-surrogates range (\uDC00-\uDFFF).  
	 */
	public static String escapeXMLEntities(String in) {
		if (in == null) return "";
		StringBuffer buf = new StringBuffer();
		for (char c: in.toCharArray()) {
			if (Character.isDefined(c)) {
				buf.append(c);
			}
		}
		return StringEscapeUtils.escapeXml(buf.toString());
	}

	/**
	 *  Escapes XML entities in a HTML-compatible way (i.e. does not escape
	 *  entities that are already escaped).
	 *  Discards non XML control characters. 
	 *
	 *  @param buf	Input string
	 *  @return	Clean and protected XML string
	 */
	public static String escapeHTMLEntities(String buf)
	{
		if (buf == null) return ""; // output of a null value
		StringBuffer tmpBuf = new StringBuffer( buf.length() + 20 );

		for( int i = 0; i < buf.length(); i++ )
		{
			char ch = buf.charAt(i);

			if (ch < ' ') {
				if (ch == '\n' || ch == '\r' || ch == '\t') {
					tmpBuf.append( ch );
				} // else DISCARD control characters because they are not allowed in XML...
			}
			else if( ch == '<' )
			{
				tmpBuf.append("&lt;");
			}
			else if( ch == '>' )
			{
				tmpBuf.append("&gt;");
			}
			else if( ch == '\"' )
			{
				tmpBuf.append("&quot;");
			}
			else if( ch == '&' )
			{
				for( int j = (i < buf.length()-1 ) ? i+1 : i; j < buf.length(); j++ )
				{
					int ch2 = buf.charAt(j);
					if( ch2 == ';' )
					{
						tmpBuf.append(ch);
						break;
					}
					if( ch2 != '#' && !Character.isLetterOrDigit( (char)ch2) )
					{
						tmpBuf.append("&amp;"); break;
					}
				}
			}
			else
			{
				tmpBuf.append( ch );
			}
		}

		return tmpBuf.toString();
	}
	
	/**
	 *  Escapes characters for safe JavaScript strings
	 *  Escape JavaScript is different than escaping for JSON!
	 *
	 *  @param buf	Input string
	 *  @return	JavaScript compatible String
	 */
	public static String escapeJavaScript(String buf)
	{
		if (buf == null) return ""; // output of a null value
		StringBuffer tmpBuf = new StringBuffer( buf.length() + 20 );
		 // \xFF does not exist in JSON and \' neither !!!
		for( int i = 0; i < buf.length(); i++ )
		{
			char ch = buf.charAt(i);

 			if( ch == '\\' )
			{
				tmpBuf.append("\\\\");
			}
			else if( ch == '\"' )
			{
				tmpBuf.append("\\\"");
			}
			else if( ch == '\'' )
			{
				tmpBuf.append("\\'");
			}
			else if( ch == '\t' )
			{
				tmpBuf.append("\\t");
			}
			else if( ch == '\b' )
			{
				tmpBuf.append("\\b");
			}
			else if( ch == '\f' )
			{
				tmpBuf.append("\\f");
			}
			else if( ch == '\r' )
			{
				tmpBuf.append("\\r");
			}
			else if( ch == '\n' )
			{
				tmpBuf.append("\\n");
			}
			else if( ch < ' ' )
			{
				tmpBuf.append("\\x");
				tmpBuf.append(Integer.toHexString(ch+0x100).substring(1).toUpperCase());
			}
			else if( ch > 0xFF )
			{
				tmpBuf.append("\\u");
				tmpBuf.append(Integer.toHexString(ch+0x10000).substring(1).toUpperCase());
			}
			else if( ch > '~' )
			{
				tmpBuf.append("\\x");
				tmpBuf.append(Integer.toHexString(ch+0x100).substring(1).toUpperCase());
			}
			else
			{
				tmpBuf.append( ch );
			}
		}

		return tmpBuf.toString();
	}

	/**
	 *  Escapes characters for safe JavaScript strings
	 *
	 *  @param buf	Input string
	 *  @return	JavaScript compatible String
	 */
	public static String escapeJSON(String buf)
	{
		if (buf == null) return ""; // output of a null value
		StringBuffer tmpBuf = new StringBuffer( buf.length() + 20 );
		 // \xFF does not exist in JSON and \' neither !!!
		for( int i = 0; i < buf.length(); i++ )
		{
			char ch = buf.charAt(i);

 			if( ch == '\\' )
			{
				tmpBuf.append("\\\\");
			}
			else if( ch == '\"' )
			{
				tmpBuf.append("\\\"");
			}
			else if( ch == '/' )
			{
				tmpBuf.append("\\/");
			}
			else if( ch == '\t' )
			{
				tmpBuf.append("\\t");
			}
			else if( ch == '\b' )
			{
				tmpBuf.append("\\b");
			}
			else if( ch == '\f' )
			{
				tmpBuf.append("\\f");
			}
			else if( ch == '\r' )
			{
				tmpBuf.append("\\r");
			}
			else if( ch == '\n' )
			{
				tmpBuf.append("\\n");
			}
			else if( ch < ' ' ) // \xFF does not exist in JSON and \' neither !!!
			{
				tmpBuf.append("\\u");
				tmpBuf.append(Integer.toHexString(ch+0x10000).substring(1).toUpperCase());
			}
			else if( ch > '~' )
			{
				tmpBuf.append("\\u");
				tmpBuf.append(Integer.toHexString(ch+0x10000).substring(1).toUpperCase());
			}
			else
			{
				tmpBuf.append( ch );
			}
		}

		return tmpBuf.toString();
	}

	public static String encodeURLnonASCII(final String s) 
	{ 
	  if (s == null) 
	  { 
	    return ""; 
	  } 
	 
	  final StringBuilder sb = new StringBuilder(); 
	 
	  try 
	  { 
	    for (int i = 0; i < s.length(); i++) 
	    { 
	      final char c = s.charAt(i); 
	 
	      if (c >= 32 && c < 128) 
	      { 
	        sb.append(c); 
	      } 
	      else 
	      { 
	        final byte[] bytes = ("" + c).getBytes("UTF-8"); 
	 
	        for (byte b : bytes) 
	        { 
	          sb.append('%'); 
	 
	          int upper = (((int) b) >> 4) & 0xf; 
	          sb.append(Integer.toHexString(upper).toUpperCase(Locale.US)); 
	 
	          int lower = ((int) b) & 0xf; 
	          sb.append(Integer.toHexString(lower).toUpperCase(Locale.US)); 
	        } 
	      } 
	    } 
	 
	    return sb.toString(); 
	  } 
	  catch (UnsupportedEncodingException uee) 
	  { 
	    throw new RuntimeException("UTF-8 unsupported!?", uee); 
	  } 
	} 
	/**
	 *  Escapes characters for safe labels in DOT language (GraphWiz.org)
	 *
	 *  @param buf	Input string
	 *  @return	GraphWiz DOT language compatible String
	 *  
	 */
	public static String escapeDot(String buf)
	{
		if (buf == null) return ""; // output of a null value
		StringBuffer tmpBuf = new StringBuffer( buf.length() + 20 );

		for( int i = 0; i < buf.length(); i++ )
		{
			char ch = buf.charAt(i);

			if( ch == '\"' )
			{
				tmpBuf.append("\\\"");
			}
			else if( ch == '\t' )
			{
				tmpBuf.append("\\t");
			}
			else if( ch == '\b' )
			{
				tmpBuf.append("\\b");
			}
			else if( ch == '\f' )
			{
				tmpBuf.append("\\f");
			}
			else if( ch == '\r' )
			{
				tmpBuf.append("\\r");
			}
			else if( ch == '\n' )
			{
				tmpBuf.append("\\n");
			}
			else if( ch >= ' ' )
			{
				tmpBuf.append( ch );
			}
		}

		return tmpBuf.toString();
	}

	public static String cleanXmlString(String in) {

		/**
		 * From Ben J. Christensen, 		http://cse-mjmcl.cse.bris.ac.uk/blog/2007/02/14/1171465494443.html     
		 * This method ensures that the output String has only
		 * valid XML unicode characters as specified by the
		 * XML 1.0 standard. For reference, please see
		 * <a href=�http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char�>the
		 * standard</a>. This method will return an empty
		 * String if the input is null or empty.
		 *
		 * @param in The String whose non-valid characters we want to remove.
		 * @return The in String, stripped of non-valid characters.
		 */

		StringBuffer out = new StringBuffer(); // Used to hold the output.

		char current; // Used to reference the current character.

		if (in == null || (in.isEmpty())) return ""; // vacancy test.

		for (int i = 0; i < in.length(); i++) {
			current = in.charAt(i); // NOTE: No IndexOutOfBoundsException caught here; it should not happen.
			if ((current == 0x9) ||
					(current == 0xA) ||
					(current == 0xD) ||
					((current >= 0x20) && (current <= 0xD7FF)) ||
					((current >= 0xE000) && (current <= 0xFFFD)) ||
					((current >= 0x10000) && (current <= 0x10FFFF)))
				out.append(current);
		}
		return out.toString();
	}

	/**
	 * Equivalent to string.replaceAll but without regular expressions
	 * 
	 * @param aInput	Input string
	 * @param aOldPattern	String to be replaced. Cannot be empty.
	 * @param aNewPattern	Replacement string
	 * @return	Input string with aOldPattern replaced by aNewPattern
	 */
	public static String replaceAllNoRegExp(
			final String aInput,
			final String aOldPattern,
			final String aNewPattern
	)
	{
		if ( aOldPattern.equals("") ) {
			throw new IllegalArgumentException("Old pattern must have content.");
		}

		final StringBuffer result = new StringBuffer();
		//startIdx and idxOld delimit various chunks of aInput; these
		//chunks always end where aOldPattern begins
		int startIdx = 0;
		int idxOld = 0;
		while ((idxOld = aInput.indexOf(aOldPattern, startIdx)) >= 0) {
			//grab a part of aInput which does not include aOldPattern
			result.append( aInput.substring(startIdx, idxOld) );
			//add aNewPattern to take place of aOldPattern
			result.append( aNewPattern );

			//reset the startIdx to just after the current match, to see
			//if there are any further matches
			startIdx = idxOld + aOldPattern.length();
		}
		//the final chunk will go to the end of aInput
		result.append( aInput.substring(startIdx) );
		return result.toString();
	}

	/**
	 * Prints in System.out (Tomcat stdout logs) the current stack when an exception was thrown
	 * 
	 * @param e	thrown exception
	 */
	public static void printStack (Exception e) {
		/*
		ByteArrayOutputStream str = new ByteArrayOutputStream();
		PrintWriter stack = new PrintWriter(str);
		e.printStackTrace(stack);
		log.error(str.toString());
		 */ e.printStackTrace(System.out);
	}

	/**
	 * Removes double (and more) spaces within a String
	 * + spaces at beginning or end of the String.
	 * 
	 * @param aValue	Input String
	 * @return	Input string with extraneous, beginning and ending spaces removed.
	 */
	public static String noDoubleSpace(String aValue) {
		//TODO: not very optimal; should be a loop on characters building a new string...
		for (int posAut = aValue.indexOf("  "); posAut >= 0; posAut = aValue.indexOf("  ")) { // remove double spaces
			aValue = aValue.substring(0,posAut)+aValue.substring(posAut+1);
		}
		return aValue.trim();
	}

	/**
	 * Normalize file names so they don't differ too much from one OS to another
	 * Windows "\" become "/"
	 * TODO: Other rules? to be implemented...
	 * 
	 * @param aValue	File name to be normalized
	 * @return	Normalized file name
	 */
	public static String fileIdentificationNormalize(String aValue) {
		char[] name = aValue.toCharArray();
		for (int posV = 0; posV < name.length; posV++) {
			if (name[posV] == File.separatorChar) name[posV] = '/'; // Normalize to UNIX file separator
		}
		return new String(name);
	}

	/**
	 * Left align a String in a fixed number of columns for HTML output (&amp;nbsp; used as a filler)
	 * 
	 * @param aValue	Input String
	 * @param columns	Number of columns
	 * @return	String filled with &amp;nbsp;
	 */
	public static String fixColumns (String aValue, int columns) {
		String aResult = "          ";
		if (aValue!=null) aResult=(aValue+aResult).substring(0,columns);
		return Util.replaceAllNoRegExp(aResult," ", "&nbsp;");
	}

	/**
	 * Prepares an array of queried words to help later highlight of text displayed
	 * 
	 * @param query	String with the current Query words
	 * @param byPieces	true if each word is searched independently
	 * @return	Array of Searched words for the "highlight" function
	 */
	public static String[] prepareHighlight(String query, boolean byPieces) {
		if (query == null) return null;
		String QUERY = Util.noSingleAccent(query.toUpperCase()).trim();
		if (QUERY.length() == 0) return null;
		StringTokenizer st = new StringTokenizer(QUERY,(byPieces? " *" : "*"));
		int nbTokens = st.countTokens();
		String[]result = new String[nbTokens];
		for (int i = 0; i < nbTokens; i++) {
			result[i] = st.nextToken();
 		}
		return result;
	}

	/**
	 * Highlight a text based on the words from a query.
	 * 
	 * @param text	Text to analyze
	 * @param QUERIES	String array prepared by method prepareHighlight
	 * @return	Text String with <code>&lt;font color="red"&gt;</code> added around highlighted text
	 * 
	 * TODO: parameterize the highlight markup
	 */
	public static String highlight (String text, String[] QUERIES) {
		if (text == null) return "";
		if (QUERIES == null) return text;
		if (QUERIES.length == 0) return text;
		StringBuffer result = new StringBuffer();
		boolean[] done = new boolean[QUERIES.length];
		for (int q=0; q < QUERIES.length; q++) {
			done[q] = QUERIES.length == 0;
		}
		boolean inTag = false;
		boolean inString = false;
		int inRed = 0;
		int pos = 0;
		int reste = text.length();
		for (char c: text.toCharArray()) {
			if (inRed > 0) {
				result.append(c);
				inRed--;
				if (inRed == 0) result.append("</font>");
			}
			else {
				if ( ! inString) {
					if (inTag) {
						if ( c == '>' ) {
							inTag = false;
						} else if (c == '"') {
							inString = true;
						}
					} else {
						if ( c == '<' ) {
							inTag = true;
						}
						else {
							for (int q=0; q < QUERIES.length; q++) {
								if (!done[q]) {
									if (reste >= QUERIES[q].length()) {
										//|| (QUERIES.length() > text.length())
										if (noSingleAccent(Character.toUpperCase(c)) == QUERIES[q].charAt(0)) {
											boolean found = false;
											for (int i=1; i<QUERIES[q].length(); i++) {
												found = false;
												if ((pos+i) >= text.length()) break;
												if (noSingleAccent(Character.toUpperCase(text.charAt(pos+i))) != QUERIES[q].charAt(i)) break;
												found = true;
											}
											if (found) {
												done[q] = true;
												result.append("<font color=\"red\">");
												inRed = QUERIES[q].length()-1;
												break;
											}
										}
									}
								}
							}
						}
					}
				} else {
					if (c == '"') inString = false;
				}
				result.append(c);
			}
			pos++;
			reste--;
		}
		return result.toString();
	}

	/**
	 * Removes all accent over (under) letters
	 *  
	 * @param input	Input String
	 * @return	"cleaned" string
	 */
	public static String noAccent(String input) {
		if (input == null) return null;
		StringBuffer output = new StringBuffer();

		for (char c : input.toCharArray()) {
			// Quick test: if it's not in range then just keep
			// current character
			if (c < '\u00c0' || c > '\uFB06')
				output.append(c);
			else {
				noAccent(c,output);
			}
		}
		return output.toString();
	}
	
	static final String jokers = "+-&|!(){}[]^\"~*?:\\";
	
	/**
	 * Removes Lucene joker characters when doing an exact match search
	 * 
	 * @param input	Input String
	 * @return	Cleaned string
	 */
	public static String noLuceneJoker(String input) {
		StringBuffer sb = new StringBuffer();
		for (char c : input.toCharArray()) {
			if (jokers.indexOf(c) >= 0) {
				sb.append('\\');
			}
			sb.append(c);
		}
		return sb.toString();
	}

	/**
	 * Brings a word with more than 3 letters to its singular form.
	 * CHEVALS becomes CHEVAL, CHEVEAUX remains CHEVEAUX !
	 * DES remains DES.
	 * Only ending "S" (upper or lower case) is removed except if it is duplicated:
	 * LESS remains LESS.
	 * LES remains LES.
	 * LEURS becomes LEUR...
	 *  
	 * @param word	Input String
	 * @return	Word without S at the end.
	 */
	public static String noPlural(String word) {
		if (word.length() > 3) {
			int wl = word.length()-1;
			if (word.charAt(wl) == 'S' || word.charAt(wl) == 's') {
				char prev = word.charAt(wl-1); 
				if (prev != 'S' && prev != 's') {
					word = word.substring(0, wl);
				}
			}
		}
		return word;
	}

	/**
	 * Use noSingleAccent for each character in a String.
	 * Used for highlighting only.
	 * 
	 * @param input	Input String to process one character at a time
	 * @return	String with accent removed. OE character may become a single O...
	 */
	public static String noSingleAccent(String input) {
		if (input == null) return null;
		StringBuffer output = new StringBuffer();

		for (char c : input.toCharArray()) {
			output.append(noSingleAccent(c));
		}
		return output.toString();
	}
	
	/**
	 * This (incorrect) version takes ONE character and removes its accents.
	 * The result is only the first resulting character instead of the whole unaccented equivalent (which may be more than one character like OE single character becoming O...)
	 * 
	 * @param c	character to process
	 * @return first character of the unaccented equivalent to c
	 */
	public static char noSingleAccent(char c) {
		// Quick test: if it's not in range then just keep
		// current character
		if (c < '\u00c0' || c > '\uFB06')
			return c;
		else {
			StringBuffer output = new StringBuffer();
			noAccent(c,output);
			return output.charAt(0);
		}
	}

	/*
	 * Unaccented version of every letter.
	 * May append more than one character (OE single character becoming "OE")
	 * 
	 * TODO: to be continuously reviewed and updated!
	 */
	private static void noAccent(char c, StringBuffer output) {
		switch (c) {
		case '\u00C0' : // �
		case '\u00C1' : //
		case '\u00C2' : // �
		case '\u00C3' : // �
		case '\u00C4' : // �
		case '\u00C5' : // �
			output.append('A');
			break;
		case '\u00C6' : // �
			output.append('A');
			output.append('E');
			break;
		case '\u00C7' : // �
			output.append('C');
			break;
		case '\u00C8' : // �
		case '\u00C9' : // �
		case '\u00CA' : // �
		case '\u00CB' : // �
			output.append('E');
			break;
		case '\u00CC' : // �
		case '\u00CD' : //
		case '\u00CE' : // �
		case '\u00CF' : //
			output.append('I');
			break;
		case '\u0132' : // 
			output.append('I');
			output.append('J');
			break;
		case '\u00D0' : //
			output.append('D');
			break;
		case '\u00D1' : // �
			output.append('N');
			break;
		case '\u00D2' : // �
		case '\u00D3' : // �
		case '\u00D4' : // �
		case '\u00D5' : // �
		case '\u00D6' : // �
		case '\u00D8' : // �
			output.append('O');
			break;
		case '\u0152' : // 
			output.append('O');
			output.append('E');
			break;
		case '\u00DE' : // 
			output.append('T');
			output.append('H');
			break;
		case '\u00D9' : // �
		case '\u00DA' : // �
		case '\u00DB' : // �
		case '\u00DC' : // �
			output.append('U');
			break;
		case '\u00DD' : //
		case '\u0178' : // �
			output.append('Y');
			break;
		case '\u00E0' : // �
		case '\u00E1' : // �
		case '\u00E2' : // �
		case '\u00E3' : // �
		case '\u00E4' : // �
		case '\u00E5' : // �
			output.append('a');
			break;
		case '\u00E6' : // �
			output.append('a');
			output.append('e');
			break;
		case '\u00E7' : // �
			output.append('c');
			break;
		case '\u00E8' : // �
		case '\u00E9' : // �
		case '\u00EA' : // �
		case '\u00EB' : // �
			output.append('e');
			break;
		case '\u00EC' : // �
		case '\u00ED' : // �
		case '\u00EE' : // �
		case '\u00EF' : // �
			output.append('i');
			break;
		case '\u0133' : //
			output.append('i');
			output.append('j');
			break;
		case '\u00F0' : // 
			output.append('d');
			break;
		case '\u00F1' : // �
			output.append('n');
			break;
		case '\u00F2' : // �
		case '\u00F3' : // �
		case '\u00F4' : // �
		case '\u00F5' : // �
		case '\u00F6' : // �
		case '\u00F8' : // �
			output.append('o');
			break;
		case '\u0153' : // �
			output.append('o');
			output.append('e');
			break;
		case '\u00DF' : // �
			output.append('s');
			output.append('s');
			break;
		case '\u00FE' : //
			output.append('t');
			output.append('h');
			break;
		case '\u00F9' : // �
		case '\u00FA' : // �
		case '\u00FB' : // �
		case '\u00FC' : // �
			output.append('u');
			break;
		case '\u00FD' : // �
		case '\u00FF' : // �
			output.append('y');
			break;
		case '\uFB00': //
			output.append('f');
			output.append('f');
			break;
		case '\uFB01': // 
			output.append('f');
			output.append('i');
			break;
		case '\uFB02': //
			output.append('f');
			output.append('l');
			break;
			// following 2 are commented as they can break the maxSizeNeeded (and doing *3 could be expensive)
		case '\uFB03': //
			output.append('f');
			output.append('f');
			output.append('i');
			break;
		case '\uFB04': //
			output.append('f');
			output.append('f');
			output.append('l');
			break;
		case '\uFB05': //
			output.append('f');
			output.append('t');
			break;
		case '\uFB06': //
			output.append('s');
			output.append('t');
			break;
		default :
			output.append(c);
		break;
		}
	}

	/**
	 * Creates a sequential list of words
	 *  
	 * @param input	Input String
	 * @return	LinkedList of the words in the input string
	 */
	public static LinkedList<String> wordSplitter(String input) {
		boolean inWord = false;
		StringBuffer curWord = null;
		LinkedList<String> result = new LinkedList<String>();
		for (char c: input.toCharArray()) {
			if (Character.isLetterOrDigit(c)) {
				if (!inWord) {
					curWord = new StringBuffer();
					inWord = true;
				}
				curWord.append(c);
			} else {
				if (inWord) {
					inWord = false;
					result.add(curWord.toString());
				}
			}
		}
		if (inWord) {
			result.add(curWord.toString());
		}
		return result;
	}

	private static final String endSentence = ".?!;";

	/**
	 * Splits a String in a list of sentences, based on the punctuation.
	 * 
	 * @param input	InputString
	 * @return	LinkedList with one String for each sentence.
	 */
	public static LinkedList<String> sentenceSplitter(String input) {
		boolean inSentence = false;
		StringBuffer curSentence = null;
		LinkedList<String> result = new LinkedList<String>();
		char prevChar = ' ';
		for (char c: input.toCharArray()) {
			if (c != ' ' || endSentence.indexOf(prevChar)< 0) {
				if (!inSentence) {
					curSentence = new StringBuffer();
					inSentence = true;
				}
				curSentence.append(c);
			} else { // c == ' ' && endSentence.indefOf(prevChar) >= 0
				if (inSentence) {
					inSentence = false;
					result.add(curSentence.toString());
				}
			}
			prevChar = c;
		}
		if (inSentence) {
			result.add(curSentence.toString());
		}
		return result;
	}

	/**
	 * Creates a sequential list of words (brought to singular form)
	 *  
	 * @param input	Input String
	 * @return	LinkedList of the words in the input string
	 */
	public static LinkedList<String> singularWordSplitter(String input) {
		boolean inWord = false;
		StringBuffer curWord = null;
		LinkedList<String> result = new LinkedList<String>();
		for (char c: input.toCharArray()) {
			if (Character.isLetterOrDigit(c)) {
				if (!inWord) {
					curWord = new StringBuffer();
					inWord = true;
				}
				curWord.append(c);
			} else {
				if (inWord) {
					inWord = false;
					result.add(noPlural(curWord.toString()));
				}
			}
		}
		if (inWord) {
			result.add(noPlural(curWord.toString()));
		}
		return result;
	}

	private static final Pattern patternA = Pattern.compile("<a\\b[^>]*>(.*?)</a>",Pattern.CASE_INSENSITIVE+Pattern.UNICODE_CASE);
	private static final Pattern patternObject = Pattern.compile("<object\\b[^>]*>(.*?)</object>",Pattern.CASE_INSENSITIVE+Pattern.UNICODE_CASE);
	private static final Pattern patternTag = Pattern.compile("</?[^>]+>");

	/**
	 * Removes HTML links from a String (<a...>...</a> and <object...>...</object>) keeping the link text.
	 * 
	 * @param input	HTML Input String
	 * @return	HTML String but without links
	 */
	public static String noHTMLLink(String input) {
		if (input == null) return null;
		
		Matcher matcher = patternA.matcher(input);
    	StringBuffer sb = new StringBuffer();
		while (matcher.find()) {
		     matcher.appendReplacement(sb, matcher.group(1));
		}
		matcher.appendTail(sb);
		matcher = patternObject.matcher(sb.toString());
    	sb = new StringBuffer();
		while (matcher.find()) {
		     matcher.appendReplacement(sb, matcher.group(1));
		}
		matcher.appendTail(sb);
		/* insert here any other tag to be removed */
		return sb.toString();
	}
	
	/**
	 * Removes HTML tags from a String (<tag...>...</tag>) keeping the texts.
	 * 
	 * @param input	HTML Input String
	 * @return	HTML String but without tags
	 */
	public static String noHTMLtag(String input) {
		if (input == null) return null;
		
		Matcher matcher = patternTag.matcher(input);
    	StringBuffer sb = new StringBuffer();
		while (matcher.find()) {
		     matcher.appendReplacement(sb, matcher.group(1));
		}
		matcher.appendTail(sb);
		return sb.toString();
	}
	private static final int longLength=21; // Length of the number in the generated Sort Key

	/**
	 * Makes a long number sortable as a string of characters. Useful to mix numbers and names within an alphabetical sort key.
	 * 
	 * @param sortKey	long number to convert
	 * @return 22 characters. The first one is '-' for negative numbers, '=' for positive ones ('=' is greater than '-'). The number itself is right aligned in 21 columns (space used as filler).
	 */
	public static String formatLong(long sortKey) {
		StringBuffer result = new StringBuffer(longLength+1);
		result.append( sortKey < 0 ? '-' : '=' );  // '+' is BEFORE '-' in ASCII !
		String longString = "";
		if (sortKey < 0) {
			longString = Long.toString( -sortKey );
		} else if (sortKey > 0) {
			longString = Long.toString( sortKey );
		}
		int fill = longLength - longString.length();
		while (fill > 0) {
			result.append(' ');
			fill--;
		}
		result.append(longString);
		return result.toString();
	}

	/**
	 * Number of digits for a number written in the given basis
	 * 
	 * @param num	Number to be sized
	 * @param basis	(1 or less is treated as 10)
	 * @return	Number of digits
	 */
	public static int magnitude(long num, int basis)  {
		int magnitude = 0;
		if (basis < 2 || basis == 10) {
			magnitude = Long.toString(num).length();
		} else if (basis == 2) {
			magnitude = 64 - Long.numberOfLeadingZeros(num - 1); 
		} else if (basis == 4) {
			magnitude = (65 - Long.numberOfLeadingZeros(num - 1)) / 2; 
		} else if (basis == 8) {
			magnitude = (66 - Long.numberOfLeadingZeros(num - 1)) / 3;	
		} else if (basis == 16) {
			magnitude = (67 - Long.numberOfLeadingZeros(num - 1)) / 4;	
		} else if (basis == 32) {
			magnitude = (68 - Long.numberOfLeadingZeros(num - 1)) / 5;	
		} else if (basis == 64) {
			magnitude = (69 - Long.numberOfLeadingZeros(num - 1)) / 6;	
		} else {
			magnitude = (int) Math.ceil(Math.log(num) / Math.log(basis));
		}
		return magnitude;
	}
	
	/**
	 * Ensures the last non alphanum character in buf is not repeated when adding toMelt
	 * For instance, to put a dot at the end of a sentence without possible repetition:
	 * <code>met(title,". ")</code> will add ". " but without repeating a dot which may already ends the current title.
	 * 
	 * @param buf	StringBuffer receiving new pieces of text
	 * @param toMelt	String to add
	 */
	public static void melt (StringBuffer buf, String toMelt) {
		if (toMelt == null || toMelt.isEmpty()) return;
		if (buf.length()==0) {
			buf.append(toMelt);
			return;
		}
		char lastBuf = buf.charAt(buf.length()-1);
		char firstMelt = toMelt.charAt(0);
		if (lastBuf != firstMelt) {
			buf.append(toMelt);
			return;
		}
		if (Character.isJavaIdentifierPart(lastBuf)) {
			buf.append(toMelt);
			return;			
		}
		buf.append(toMelt.substring(1));
	}
	
	/**
	 * Glue an array of strings (lines) into one string
	 * 
	 * @param toGlue String Array of the lines to "glue"
	 * @param delim	String inserted between each line (array element)
	 * @return	Glued string
	 */
	public static String glue (String[]toGlue, String delim) {
		StringBuffer sb = new StringBuffer();
		boolean first = true;
		for (String aSeg : toGlue) {
			if (first) {
				first = false;
			} else {
				sb.append(delim);
			}
			sb.append(aSeg);
		}
		return sb.toString();		
	}
	
	private static final String endSegment = ".,;?!:+])}%$�";
	
	/**
	 * Wraps a string into a string array representing lines of about maxWidth characters (words are not cut)
	 * 
	 * @param input	Input String
	 * @param maxWidth	Approximate width of a line
	 * @return	Array of Strings (each element is one line)
	 */
	public static String[] wordWrap(String input, int maxWidth) {
		int marginalShort = maxWidth * 3 / 10;
		String[] words = input.split(" ");
		LinkedList<String> lines = new LinkedList<String>();
		String currLine = "";
		for (String oneWord : words) {
			if (oneWord.isEmpty()) continue;
			if (currLine.isEmpty()) {
				currLine = oneWord;
			} else {
				if ((currLine.length()+oneWord.length()) >= maxWidth) {
					if (endSegment.indexOf(oneWord.charAt(oneWord.length()-1)) >= 0) {
						lines.add(currLine+' '+oneWord);
						currLine = "";
					} else {
						lines.add(currLine);
						currLine = oneWord;
					}
				} else {
					currLine = currLine + ' ' + oneWord;
					if ((currLine.length() >= (maxWidth - marginalShort))
						&& endSegment.indexOf(oneWord.charAt(oneWord.length()-1)) >= 0) {
						lines.add(currLine);
						currLine = "";
					}
				}
			}
		}
		if (!currLine.isEmpty()) {
			lines.add(currLine);
		}
		return lines.toArray(new String[0]);
	}
	
	/**
	 * Convert an array of String in a LinkedHashSet of String
	 * 
	 * @param anArray	String Array to convert
	 * @return	LinkedHashSet with those strings.
	 */
	public static LinkedHashSet<String> hashStrings ( String[] anArray ) {
		LinkedHashSet<String> aSet = new LinkedHashSet<String>();
		for (String anEntry : anArray) {
			aSet.add(anEntry);
		}
		return aSet;
	}
	
	/**
	 * If a letter is the very first character, puts it in Upper case.
	 * 
	 * @param name	Input String
	 * @return	Input string with very first character in Upper case
	 */
	public static String firstUp (String name) {
		if (name == null || name.isEmpty()) return name;
		return Character.toUpperCase(name.charAt(0)) + name.substring(1);
	}
	
	/**
	 * Search a byte array buffer for a byte array pattern, from a base position.
	 * Beware, UTF-8 characters can be composed of multiple bytes.
	 *  
	 * @param buffer	
	 * @param len	End of the buffer part to search
	 * @param pattern	Byte array with pattern to search. (if empty, search fails)
	 * @param base	Position in the buffer where the search starts (0 is the first buffer position)
	 * @return	Position of the first byte of pattern in buffer
	 */
	public static int searchBuffer (byte[] buffer, int len, byte[] pattern, int base) {
		int lp = pattern.length;
		if (lp <= 0) return -1;
		if (len > buffer.length) {
			len = buffer.length;
		}
		for (int i=base; i <= (len-lp); i++) {
			if (buffer[i]==pattern[0]) {
				boolean gotIt = true;
				for (int j=1; j < lp; j++) {
					if (buffer[i+j]!=pattern[j]) {
						gotIt = false;
						break;
					}
				}
				if (gotIt) return i;
			}
		}
		return -1;
	}

	/**
	 * Move a byte array buffer for a byte array pattern, from a base position.
	 * Beware, UTF-8 characters can be composed of multiple bytes.
	 *  
	 * @param buffer	
	 * @param len	End of the buffer part to search
	 */
	public static void moveBuffer (byte[] buffer, int len, int orig, int dest) {
		if (dest == orig) return;
		if (dest < orig) {
			int j = 0;
			for (int i=orig; i < len; i++) {
				buffer[dest+j] = buffer[i];
				j++;
			}			
		} else {  // TODO !!!
		}
	}
	/**
	 * This function returns an URL with [ ] replaced by ( ) so no more substitution may happen
	 * return the cleaned URL
	 */
	public static String noMoreSubstitute(String input) {
		if (input == null) return null;
		return replaceAllNoRegExp( replaceAllNoRegExp(input, ""+Reflector.substBeg, "(") , ""+Reflector.substEnd, ")");
	}

    /**
     *  Returns the ServletContext Init parameter if has been set, otherwise
     *  checks for a System property of the same name. If neither are defined,
     *  returns null. This permits both Servlet- and System-defined cascading
     *  properties.
     */
    public static String getInitParameter( HttpServletRequest req, String name )
    {
    	String value = null;
    	if (req != null) {
    		ServletContext context = req.getServletContext();
    		if(context != null) {
    			value = context.getInitParameter(name);
    		}
    	}
    	if (value != null) return value;
        return System.getProperty(name) ;
    }

	public static String getURLroot(HttpServletRequest req) {

	    String scheme = req.getScheme();             // http
	    String serverName = req.getServerName();     // hostname.com
	    int serverPort = req.getServerPort();        // 80
	    String contextPath = req.getContextPath();   // /mywebapp
	    /*
	    String servletPath = req.getServletPath();   // /servlet/MyServlet
	    String pathInfo = req.getPathInfo();         // /a/b;c=123
	    String queryString = req.getQueryString();          // d=789
	    */

	    // Reconstruct original requesting URL
	    StringBuffer url =  new StringBuffer();
	    url.append(scheme).append("://").append(serverName);

	    if (serverPort != 80) {
	        url.append(":").append(serverPort);
	    }

	    url.append(contextPath);
/*.append(servletPath)
	    if (pathInfo != null) {
	        url.append(pathInfo);
	    }
	    if (queryString != null) {
	        url.append("?").append(queryString);
	    }
 */
	    return url.toString();
	}

	public static String slashIt(String url) {
		if (url == null) return "/";
		url = url.trim();
		if (url.isEmpty()) return "/";
		if (url.charAt(url.length()-1) != '/') url += '/';
		return url;
	}

	public static String noSlashIt(String url) {
		if (url == null) return null;
		url = url.trim();
		if (url.isEmpty()) return null;
		if (url.charAt(url.length()-1) == '/') return url.substring(0,url.length()-1);
		return url;
	}

}
